Ana içeriğe geç

Lifetimes

Bu başlığımızda lifetime ifadelerinden bahsedeceğiz. Dikkat etmemiz gereken şey Rust üzerindeki her bir referansın belirli bir Life time'a yani yaşam süresine sahip olduğudur. Çoğu zaman lifetime'lar üstü kapalıdır ve çok bahsedilmez. Bazıları da bu özelliğin Rust'ı en çok diğerlerinden ayıran özelliği olduğunu söylerler.

Liftime'ların ana olarak odaklandığı şeyler dangling referance yani sahipsiz referansları önlemektir. Hadi örnek olarak dangling refferance oluşturalım.

fn main(){
let r;
{
let x = 5;
r = &x;
}
println!("{}", r)
}

Burada kodumzu çalıştırdığımızda "barrowed value does not live long enough" hatası ile karşılaşırız. Bu durumun nedeni x'in tanılıldığı parantez biter bitmez x değerinin düşürülerek referansını da yok etmişizdir. Ancak sonrasında yazdırmaya çalıştığımızda olmayan bir referansı yazdıramayacağımız için hata oluşmuştur. Bu işlemi lifetime sayesinde daha iyi anlayabiliriz ve Rust Compiler bize daha kod çalışmadan bu durumu bildirir.

Lifetime yazımları daha da incelediğimizde bu yapıların bir referansın ne kadar yaşayacağını değiştirmeyeceğini unutmamamız gerekir. Lifetime yazımlar sadece birden fazla referansın birbiri ile ilişkisini yaşam süresinin değiştirmeden ifade etmektedir.

Bir lifetime oluşturmak için genelde 'a yapısı kullanılmaktadır. Bu bağlamda alt kısımdaki gibi yazımlar yapabiliriz:

  • &i32: Bir 32 bit integer değerine referans
  • &'a i32: Bir 32 bit integer değerine özel lifetime ile referans
  • &'a mut i32: Bir 32 bit integer değerine özel lifetime ile değişebilir referans

Hadi bir fonksiyon oluşturarak bu kavramların biraz daha neleri ifade ettiğine bakalım:

fn örnek<'a>(x: &'a str) -> &'a str{
return x
}

Life time'lar birbirine bağlandığı değerlernden en az kim yaşıyorsa ona kendini bağlamaktadır. Normalde onlarsız da kod teoride çalışabilse de Rust Compiler'ın aklı karışır ve anlayamaz. Bu nedenle ona anlatmamız gerekir. Bu life'time değerlerini fonksiyonlara verirken generic tpye olduğu için fonksiyon isminden hemen sonra <> içerisine yazarız .Biraz karışık bir kavram gibi gelebilir ancak örnekler ile ifade edeceğim.

Diyelim ki aileniz ile 3 araba yola çıktınız ve Antalya Trabzon yolunu kullanacaksınız. Bir arabayı siz, bir arabayı ablanız, bir arabayı babanız kullanıyor. Hepiniz "Antalya - Konya - Aksaray - Nevşehir - Kayseri - Sivas - Erzincan - Erzurum - Bayburt - Trabzon" yolunu kullanacaksınız. Ama siz Trabzona gidecekken babanız cağ kebap yemek için Erzurum'da, ablanız sucuk yemek için Kayseri'de durdu ve orada kalacaklar. Anneniz de dedi ki oğlum/kızım gelin hep birlikte bir aile fotoğrafı çektirelim yoldayken. Bu aile fotoğrafını küçük kardeşiniz çekecek ama bilmiyor hangi şehir nerede ve hangi şehir kimden sonra geliyor. Küçük kardeşimiz artık bir Rust Compiler. Küçük kardeşiniz yolu bilmediği için fotoğraf çekme görevi altında kendini ezilmiş hissediyor ve ağlayarak herkesin yola çıkmamasını sağlıyor. Sizin yapmanız gereken şey aile fotoğrafı için her bir arabanın arka arkaya gideceği yolları kardeşinize anlatmak ve en erken ayrılacak (yani lifetime'ı en kısa sürecek) ablanızdan dolayı Kayseriye kadar çekebileceğimizi söylediniz. Artık kardeşiniz anladı ve Kayseriye kadar anneniz ne zaman derse desin kardeşiniz fotoğrafı çekecek. Ama eğer kayseriden sonra derse artık çekemeyeceğini söyleyerek ağlamaya devam edecek ve yolunuzu mahfedecek.

Örneğimizde biraz daha net aklınıza oturduysa compiler birbirine aynı etiket ile bağlanan değerleri görecek ve bunlar arasında en az hayatta kalana göre hepsi sanki o kadar yaşasacakmış gibi davranacaktır. Bu nedenle hatayı en az yaşayacağın yaşadığı süre boyunca göstermeyecektir.

Peki nedir bu lifetime ve neden Rust içerisinde var? Normalde önceden Rust'ın ilk sürümlerinde liftime her bir değer için kullanılmaktaydı. Ancak Rust geliştiricileri yazılımcıların sürekli olarak benzer yapılar için aynı lifetime yazımları kullandığını fark ederek compiler içerisine bazı yazımları gömdü. Bu sayede bu sayede Rust yazılımcıları her seferinde kod içerisine bu tanımları eklemekten kurtuldu.

Bu lifetime değerlerinin compiler tarafından algılanması için ana olarak 3 kuralı bulunmaktadır.

  • Her bir parametre referansı kendi lifetime parametresini almaktadır. Örneğin tek parametremiz varsa 'a ifadesini kullanabiliriz. Ancak eğer iki parametremiz varsa 'a ve 'b ifadelerini kullanabiliriz. Kaç parametremiz varsa o kadar lifetime ifadesine sahip olabiliriz.

  • Çıktı olarak tanımlanan değere sadece bir adet lifetime ifadesi tanımlanabilir.

  • Eğer birden çok lifetime referansı varsa ve bunlardan biri self yapısına veya değitirilemez self yapısına referans ediyorsa bu referansın bir methoda hizmet etmesinden olayı tüm parametrelerin lifetime referansı self değerinin lifetime referansına eşit olur.

Hadi bir örnek ile buna bakalım

fn ornek<'a,'b>(x: &'a str, y: &'b str)-> &'b str{
return x //error
}
fn ornek<'a,'b>(x: &'a str, y: &'b str)-> &'a str{
return x // Hata yok
}

Aynı zamanda struct yapıları da kendi içerilerinde referans tutabilirler. Ancak bu işlemi gerçekleştirmek için lifetime yazıma ihtiyaç duymaktadırlar.

struct Stringim {
text: &str // Error
}
struct Stringim<'a> {
text: &'a str // Error
}

Bir değişkenin sonsuza kadar yaşamasını istiyorsak onu statik lifetime değerinine çevirebiliriz. Bu işlemi gerçekleştirmek için &'static ifadesini kullanabiliriz. Bu sayede referansımız kodumuz açık kaldığı sürece varlık gösterecektir.

let s: &'static str = "Statik yaşarım ben anı yaşarım ben"